package vargapeter.bet.server;

import java.rmi.AccessException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;

import vargapeter.bet.BookmakerRemote;
import vargapeter.bet.Log;
import vargapeter.bet.SportEventBase;

/**
 * A fogadóirodákat megvalósító osztály. Implementálja a BookmakerRemote interface-t, amin keresztül elérhetőek távolról is a szolgáltatásai. Ez az osztály valósítja meg a kommunikációt a fogadókkal
 * és a sporteseményekkel.
 */
public class Bookmaker extends UnicastRemoteObject implements BookmakerRemote {
	private String name;
	private int port;
	private int maxMoneyPerBet;
	private int p;
	private int moneyChange; /* nyílvántartjuk, hogy mennyivel változott a fogadóiroda pénze */
	private Registry registry;

	/* Az események nyílvántartása */
	private List<SportEventBase> sportEvents = new ArrayList<SportEventBase>();

	/* A fogadások nyílvántartása */
	private Map<BetterAndEvent, OutcomeAndSum> bets = new HashMap<BetterAndEvent, OutcomeAndSum>();

	/* A kimenetelek nyílvántartása */
	private Map<String, String> outcomes = new HashMap<String, String>();

	public Bookmaker(String name, int port, int maxMoneyPerBet, int p) throws RemoteException {
		super();
		this.name = name;
		this.port = port;
		this.maxMoneyPerBet = maxMoneyPerBet;
		this.p = p;
	}

	/**
	 * Elindítja a szervert a megadott porton bookmakerServer néven.
	 */
	public void startService() {
		try {
			registry = LocateRegistry.createRegistry(port);
			registry.rebind("bookmakerServer", this);
		} catch (AccessException e) {
			e.printStackTrace();
		} catch (RemoteException e) {
			e.printStackTrace();
		}
	}

	public String getName() {
		return name;
	}

	/* SPORTESEMÉNYEK */
	public synchronized void eventReleased(String eventName, int home, int away, int draw) throws RemoteException {
		int[] calculatedOdds = calcOdds(home, away, draw);
		sportEvents.add(new SportEventBase(eventName, calculatedOdds[0], calculatedOdds[1], calculatedOdds[2]));
		Log.logForServer(getName(), "\t", "Megjelent: ", eventName, home, away, draw);
	}

	public synchronized void eventEnded(String eventName, String outcome) throws RemoteException {
		/* Visszafelé iterálok a listán, hogy ki tudjam venni az végetért eseményt. */
		for (int i = sportEvents.size() - 1; i >= 0; i--) {
			SportEventBase eventBase = sportEvents.get(i);
			if (eventBase.getEventName().equals(eventName)) {
				outcomes.put(eventName, outcome);
				Log.logForServer(getName(), "\t", "Lezajlott: ", eventName, outcome);
				/* Ha egy eseménynek vége, többé nem lehet rá fogadni, úgyhogy kiveszem a listából. */
				sportEvents.get(i).setBetAllowed(false);
			}
		}

		/* végigmegyek a fogadásokon, megkeresem azt az eseményt ami épp lezajlik és ha egy fogadás nem nyert akkor hozzáadom az iroda pénzéhez */
		Set<BetterAndEvent> betsKeyset = bets.keySet();
		for (BetterAndEvent betterAndEvent : betsKeyset) {
			if (betterAndEvent.getEvent().equals(eventName)) {
				OutcomeAndSum outcomeAndSum = bets.get(betterAndEvent);
				if (!outcomeAndSum.getOutcome().equals(outcomes.get(eventName))) {
					int sum = bets.get(betterAndEvent).getSum();
					moneyChange += sum;
					Log.logForServer(getName(), "\t", "Iroda nyert: ", sum);
				}
			}
		}
	}

	/* FOGADÓK */
	public synchronized String[] sportEvents() throws RemoteException {
		List<String> eventNames = new ArrayList<String>();
		for (int i = 0; i < sportEvents.size(); i++) {
			SportEventBase event = sportEvents.get(i);
			if (event.isBetAllowed()) {
				eventNames.add(event.getEventName());
			}
		}
		String[] eventNamesArray = new String[eventNames.size()];
		Log.logForServer(getName(), "\t", "Sportesemények: ", Arrays.toString(eventNames.toArray(eventNamesArray)));

		return eventNamesArray;
	}

	public synchronized int[] odds(String eventName) throws RemoteException {
		if (eventName == null) {
			throw new RuntimeException("Az esemény neve nem lehet null!");
		}

		SportEventBase sportEvent = null;
		/* Végignézem az eseményeket és ha találtam megfelelőt akkor visszaadom az odds-okat */
		for (int i = 0; i < sportEvents.size() && sportEvent == null; i++) {
			if (eventName.equals(sportEvents.get(i).getEventName())) {
				sportEvent = sportEvents.get(i);
			}
		}

		if (sportEvent == null) {
			throw new RuntimeException("Nem létezik a megadott névvel sportesemény!");
		}

		return calcOdds(sportEvent.getHomeOdds(), sportEvent.getAwayOdds(), sportEvent.getDrawOdds());
	}

	public synchronized void bet(String betterName, String eventName, String outcome, int sum) throws RemoteException {
		if (sum <= maxMoneyPerBet) {
			bets.put(new BetterAndEvent(betterName, eventName), new OutcomeAndSum(outcome, sum));
			Log.logForServer(getName(), "\t", "Fogad: ", betterName, eventName, outcome, sum);
		}
		else {
			Log.logForServer("Ekkora összeggel nem lehet fogadni:", sum);
		}
	}

	public synchronized int isWon(String betterName) throws RemoteException {
		int sum = 0;

		for (Entry<BetterAndEvent, OutcomeAndSum> bet : bets.entrySet()) {
			if (bet.getKey().getBetter().equals(betterName)) {
				for (SportEventBase event : sportEvents) {
					if (event.getEventName().equals(bet.getKey().getEvent())) {
						sum += calculateWinning(bet.getValue(), event);
						bets.remove(bet);
					}
				}
			}
		}

		if (sum > 0) {
			moneyChange -= sum;
			Log.logForServer(getName(), "\t", "Iroda vesztett: ", sum);
		}
		return sum;
	}

	/* SEGÉDMETÓDUSOK */

	/**
	 * Visszaadja a fogadóiroda által, odds-al megnövelt pénzt amennyiben nyert fogadásról van szó. Ha a fogadó veszített, 0-át ad vissza.
	 * 
	 * @param outcomeAndSum
	 * @param event
	 * @return
	 */
	private int calculateWinning(OutcomeAndSum outcomeAndSum, SportEventBase event) {
		int sumToAdd = outcomeAndSum.getSum();
		if (SportEventBase.HOME.equals(outcomeAndSum.getOutcome())) {
			sumToAdd = (int) (sumToAdd * (event.getHomeOdds() / 100f));
		}
		if (SportEventBase.DRAW.equals(outcomeAndSum.getOutcome())) {
			sumToAdd = (int) (sumToAdd * (event.getDrawOdds() / 100f));
		}
		if (SportEventBase.AWAY.equals(outcomeAndSum.getOutcome())) {
			sumToAdd = (int) (sumToAdd * (event.getAwayOdds() / 100f));
		}

		return sumToAdd;
	}

	/**
	 * Visszaadja az összesen p százalékponttal lecsökkentett oddsokat.
	 * 
	 * @param home
	 * @param away
	 * @param draw
	 * @return Torzított oddsok. A sorrend: home, away, draw.
	 */
	private int[] calcOdds(int home, int away, int draw) {
		int[] partsOfP = getPartsOfP();
		int[] answer = new int[3];

		/* A partsOfP felosztja a p-t 3 véletlen részre és minden oddsot lecsökkentünk ezekkel a részekkel */
		answer[0] = Math.round(home - home * partsOfP[0] / 100);
		answer[1] = Math.round(away - away * partsOfP[1] / 100);
		answer[2] = Math.round(draw - draw * partsOfP[2] / 100);

		return answer;
	}

	private int[] getPartsOfP() {
		Random rnd = new Random();
		int[] parts = new int[3];
		/* A p-t 3 részre bontjuk, úgy, hogy ne minden rész legalább 1 legyen */
		parts[0] = 1 + rnd.nextInt(p - 1); /* 1 és p - 2 zárt intervallumba esik */
		parts[1] = 1 + rnd.nextInt(parts[0] + 1); /* 1 és p-1 zárt intervallumba esik */
		parts[2] = p - parts[0] - parts[1]; /* a maradék */

		return parts;
	}

	/**
	 * Az osztály nyílvántartja a fogadót és hogy milyen eseményre fogadott.
	 */
	private class BetterAndEvent {
		private String better;
		private String event;

		public BetterAndEvent(String better, String event) {
			super();
			this.better = better;
			this.event = event;
		}

		public String getBetter() {
			return better;
		}

		public String getEvent() {
			return event;
		}
	}

	/**
	 * Az osztály segít nyílvántartani az adott kimenetelhez tartozó megtett összegeket.
	 */
	private static class OutcomeAndSum {
		private String outcome;
		private int sum;

		public OutcomeAndSum(String outcome, int sum) {
			super();
			this.outcome = outcome;
			this.sum = sum;
		}

		public String getOutcome() {
			return outcome;
		}

		public int getSum() {
			return sum;
		}
	}

	/**
	 * Visszaadja a fogadóiroda portját. Ez kell a klienseknek, hogy kapcsolódni tudjanak.
	 * 
	 * @return port
	 */
	public Integer getPort() {
		return port;
	}
}
